import { dom } from '@knno/dom';
import { DATA } from '../components/symbol';

let idx = 0;

let fontZoom: number | undefined;

export function getFontZoom(): number {
	if (fontZoom === undefined) {
		const div = dom.div().css({ fontSize: '10px', height: '1px', width: '1px', opacity: 0 });
		div.appendTo(document.body);
		fontZoom = parseFloat(window.getComputedStyle(div.elems()[0]).fontSize) / 10;
		div.detach();
	}
	return fontZoom;
}

export function genId(prefix?: string): string {
	const p = prefix ? prefix + '-' : 'id';
	return `${p}${++idx}`;
}

export function getFontSize(element: HTMLElement): number {
	const sz =
		window.getComputedStyle(element).fontSize || window.getComputedStyle(document.documentElement).fontSize || '14px';
	return parseFloat(sz);
}

export function em2px(element: HTMLElement, em: number): number {
	return getFontSize(element) * em;
}

export function rem2px(rem: number): number {
	return getFontSize(document.documentElement) * rem;
}

export function size2px(
	size: number | string | undefined,
	refElem: HTMLElement,
	vertical: boolean
): number | undefined {
	if (size === undefined) {
		return undefined;
	} else if (typeof size === 'number') {
		return size;
	} else if (/\d+(\.\d+)?px$/.test(size)) {
		return parseFloat(size);
	} else if (/\d+(\.\d+)?%$/.test(size)) {
		return refElem[vertical ? 'clientHeight' : 'clientWidth'] * (parseFloat(size) / 100);
	} else if (/\d+(\.\d+)?rem$/.test(size)) {
		return rem2px(parseFloat(size));
	} else if (/\d+(\.\d+)?em$/.test(size)) {
		return em2px(refElem, parseFloat(size));
	} else if (/\d+(\.\d+)?vw$/.test(size)) {
		return window.innerWidth * (parseFloat(size) / 100);
	} else if (/\d+(\.\d+)?vh$/.test(size)) {
		return window.innerHeight * (parseFloat(size) / 100);
	} else {
		return undefined;
	}
}

const validSize = /^\d+(\.\d+)?(px|em|rem|vh|vw|%)$/;

export function getMaxHeight(elem: HTMLElement): number {
	const style = window.getComputedStyle(elem);
	const mh = validSize.test(style.maxHeight) ? parseFloat(style.maxHeight) : Infinity;
	const h = validSize.test(elem.style.height) ? parseFloat(style.maxHeight) : Infinity;
	return Math.min(mh, h);
}

export function isInSvg(node: Element): boolean {
	let p: Element | null = node;
	while (p !== null) {
		if (p.nodeName.toLowerCase() === 'svg') {
			return true;
		}
		p = p.parentElement;
	}
	return false;
}

export function isDescendant(ancestors: Node[], descendant: Node | null): boolean {
	let p: Node | null = descendant;
	while (p !== null) {
		if (ancestors.includes(p)) {
			return true;
		}
		p = p.parentElement;
	}
	return false;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function applyMixins(derivedCtor: any, baseCtors: any[]) {
	baseCtors.forEach((baseCtor) => {
		Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
			derivedCtor.prototype[name] = baseCtor.prototype[name];
		});
	});
}

/**
 * Get element offset X related of parent element
 * @param elem child element
 * @param parent parent element
 * @returns the offset X
 */
export function getOffsetX(elem: HTMLElement, parent: HTMLElement): number {
	function getOffsetLeft(el: HTMLElement) {
		let x = el.offsetLeft;
		while (el.offsetParent) {
			el = el.offsetParent as HTMLElement;
			x += el.offsetLeft;
		}
		return x;
	}
	return getOffsetLeft(elem) - getOffsetLeft(parent);
}

/**
 * Get element Y offset related of parent element
 * @param elem child element
 * @param parent parent element
 * @returns the Y offset
 */
export function getOffsetY(elem: HTMLElement, parent: HTMLElement): number {
	function getOffsetTop(el: HTMLElement) {
		let y = el.offsetTop;
		while (el.offsetParent) {
			el = el.offsetParent as HTMLElement;
			y += el.offsetTop;
		}
		return y;
	}
	return getOffsetTop(elem) - getOffsetTop(parent);
}

type EventCallback<K, V> = (event: K, data: V) => void;
type EventHandler<M> = Partial<{
	[K in keyof M]: EventCallback<K, M[K]>[];
}>;
type UnSubscriber = { unSubscribe(): void };

export class EventBus<M extends Record<string, unknown>> {
	private [DATA]: EventHandler<M> = {};
	subscribe<K extends keyof M>(event: K, callback: EventCallback<K, M[K]>) {
		const subscribers = this[DATA][event] ?? [];
		this[DATA][event] = subscribers;
		subscribers.push(callback);
		return {
			unSubscribe() {
				const idx = subscribers.findIndex((s) => s === callback);
				if (idx >= 0) {
					subscribers.splice(idx, 1);
				}
			},
		};
	}
	subcribeOnce<K extends keyof M>(event: K, callback: EventCallback<K, M[K]>, timeout?: number): UnSubscriber {
		const revoker = this.subscribe(event, (callbackEvent, data) => {
			revoker.unSubscribe();
			callback(callbackEvent, data);
		});
		if (typeof timeout === 'number' && timeout >= 0) {
			setTimeout(() => {
				revoker.unSubscribe();
			}, timeout);
		}
		return revoker;
	}
	send<K extends keyof M>(event: K, data: M[K]) {
		const subscribers = this[DATA][event];
		if (subscribers) {
			subscribers.forEach((s) => {
				s(event, data);
			});
		}
	}
	publish<K extends keyof M>(event: K, data: M[K], delay = 0) {
		setTimeout(() => {
			this.send(event, data);
		}, delay);
	}
}

// const b64Arr = [
// 	65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99,
// 	100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 48,
// 	49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47,
// ];

// export function hex2b64Ex(str: string): string {
// 	// let result = '';
// 	let previous = 0;
// 	let remainder = 0;
// 	let pos = 0;
// 	let len = Math.ceil(((str.length / 2) * 8) / 6);
// 	len += 4 - (len % 4);
// 	const buf = new Uint8Array(len);
// 	for (let i = 0; i + 1 < str.length; i += 2) {
// 		const v = parseInt(str.substring(i, i + 2), 16);
// 		const used = 6 - remainder;
// 		const idx = (((previous << (8 - remainder)) & 0xff) >> 2) | (v >> (8 - used));
// 		buf[pos++] = b64Arr[idx];
// 		remainder = 8 - used;
// 		if (remainder === 6) {
// 			const idx = ((v << (8 - remainder)) & 0xff) >> 2;
// 			buf[pos++] = b64Arr[idx];
// 			remainder = 0;
// 		} else {
// 			previous = v;
// 		}
// 	}
// 	const idx = ((previous << (8 - remainder)) & 0xff) >> 2;
// 	buf[pos++] = b64Arr[idx];
// 	for (; pos < len; pos++) {
// 		buf[pos] = '='.charCodeAt(0);
// 	}
// 	const utf8decoder = new TextDecoder('ascii');
// 	return utf8decoder.decode(buf);
// }

export function hex2bin(str: string): Uint8Array {
	const arr = new Uint8Array(Math.floor(str.length / 2));
	for (let i = 0; i + 1 < str.length; i += 2) {
		const v = parseInt(str.substring(i, i + 2), 16);
		arr[i / 2] = v;
	}
	return arr;
}

export function hex2blob(hex: string, mime: string): Blob {
	return new Blob([hex2bin(hex)], { type: mime });
}
